在正式環境,我們不再用 localStorage,改由後端回傳 Cookie
Token 由 瀏覽器自動附帶,JS 完全碰不到,安全性提升一個等級。
import express from "express";
import admin from "firebase-admin";
import dotenv from "dotenv";
dotenv.config();
const router = express.Router();
const COOKIE_NAME = "token"; // 你也可以換成 idToken
const COOKIE_MAX_AGE = 60 * 60 * 1000; // 1 小時
// 驗證 + 設置 Cookie
router.post("/verify", async (req, res) => {
try {
const { idToken } = req.body;
if (!idToken) return res.status(400).json({ message: "缺少 idToken" });
const decoded = await admin.auth().verifyIdToken(idToken);
// 寫入 HttpOnly Cookie
res.cookie(COOKIE_NAME, idToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production", // 正式環境會用 HTTPS
sameSite: "strict",
maxAge: COOKIE_MAX_AGE,
});
res.json({
uid: decoded.uid,
email: decoded.email,
message: "驗證成功,Token 已寫入 Cookie",
});
} catch (err) {
res.status(401).json({ message: "Token 驗證失敗", error: err.message });
}
});
// 登出:清除 Cookie
router.post("/logout", (req, res) => {
res.clearCookie(COOKIE_NAME);
res.json({ message: "已登出,Cookie 已清除" });
});
export default router;
只要寫在 Express 主檔案中即可:
import express from "express";
import authRoutes from "./authRoutes.js";
import cookieParser from "cookie-parser";
const app = express();
app.use(express.json());
app.use(cookieParser());
app.use("/auth", authRoutes);
app.listen(3000, () => console.log("Server on 3000"));
import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
export const loginAndSetCookie = async (email, password) => {
const auth = getAuth();
const result = await signInWithEmailAndPassword(auth, email, password);
const idToken = await result.user.getIdToken();
// 把 Token 丟去後端,由後端寫入 Cookie
const res = await fetch("/auth/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include", // 重點!
body: JSON.stringify({ idToken }),
});
if (!res.ok) throw new Error("驗證失敗");
return await res.json(); // 可取得 uid / email / message
};
export const getProtectedData = async () => {
const res = await fetch("/api/protected", {
method: "GET",
credentials: "include",
});
if (!res.ok) throw new Error("未授權");
return res.json();
};
後端就能從 Cookie 拿 Token:
router.get("/protected", async (req, res) => {
const token = req.cookies?.token;
if (!token) return res.status(401).json({ message: "沒有 Token" });
try {
const decoded = await admin.auth().verifyIdToken(token);
res.json({ message: "你進來了!", user: decoded });
} catch (err) {
res.status(401).json({ message: "Token 無效" });
}
});
export const logout = async () => {
await fetch("/auth/logout", {
method: "POST",
credentials: "include",
});
};